[reTerminal] PKCS#11を用いて秘密鍵を安全に管理しつつAWS IoTとMQTT通信する
はじめに
マルツオンライン様のGWセールでreTeminalが特価でしたので、購入してみました。reTeminalの詳細は、以下をご参照ください。
reTerminalには、ハードウェアベースの安全なキー・ストレージが付いています。こちらを利用したブログネタを探していたところ、aws-iot-device-sdk-python-v2でPKCS#11のサポートがされていましたので、試すことにしました。
PKCS#11とは
PKCS(Public Key Cryptography Standards)は、RSAセキュリティにより考案され公開された公開鍵暗号標準です。中でもPKCS#11は、HSM(Hardware Security Module)のためのAPIとなります。 IoTでは、認証情報を不正に取得されないように、認証情報をTPM(Trusted Platform Module)やHardware Security Module(HSM)といったハードウェア保護モジュールを利用することがあります。
冒頭で述べた通り、reTerminalのATECC608Aが、このハードウェア保護モジュールにあたります。本記事では、各種ツールのPKCS#11のAPIを利用して、秘密鍵を安全にデバイス側で格納しつつAWS IoTとMQTT通信を実現します。
環境
reTerminalのOSはRaspbianです。ただ、Raspberry Piを持っている方が本記事をそのまま利用することはできません。参考程度にとどめてください。
$ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster $ openssl version OpenSSL 1.1.1n 15 Mar 2022
reTerminalの初期設定
起動からwifi設定
電源をつけて、wifi設定まで行ってください。Zennでスクラップを書いたので、以下をご参照ください。
ATECC608Aのプロビジョニング
seeed様の記事、項目「デバイス秘密鍵とデバイス公開鍵を確認する」まで進めてください。
以下のshared objectファイルが存在するか確認します。後述するopensslの設定で利用します。(もしかしたら環境差異があるかもしれません)
- lib11 ->
/usr/lib/arm-linux-gnueabihf/engines-1.1/libpkcs11.so
- cryptoauthlib ->
/usr/lib/libcryptoauth.so
p11tool
で、以下のコマンドを実行し、Object 0にPrivate key、Object 1にPublic keyが割り当てられているのが確認できましたら、次の項目に進みます。
$ p11tool --provider /usr/lib/libcryptoauth.so --list-all $ p11tool --list-all "pkcs11:token=MCHP"
(余談) 紹介した記事にもある通り、reTerminalのATECC608Aは未プロビジョニング品となっています。私は業務でプロビジョニング済みの製品を利用していたため、R/W可能なスロットにデータが書き込めず混乱しました。結果この記事の存在に気付くことなく、自己流でプロビジョニングしました。。プロビジョニング状態の確認は、記事の項「ロック状態を確認」を参考にしてください。
MQTT接続準備
cryptoauthlibの再ビルド
項「ATECC608Aのプロビジョニング」で実施したcryptoauthlibをビルドしなおします。v3.3.3(リビジョン:055dd4af)の場合、MQTT通信時以下のようなエラーが発生するためです。原因については分かり次第、追記したいと思います。
Loading PKCS#11 library '/usr/lib/libcryptoauth.so' ... Loaded! Traceback (most recent call last): File "pkcs.py", line 39, in <module> connect_future.result() File "/usr/lib/python3.7/concurrent/futures/_base.py", line 432, in result return self.__get_result() File "/usr/lib/python3.7/concurrent/futures/_base.py", line 384, in __get_result raise self._exception awscrt.exceptions.AwsCrtError: AWS_ERROR_PKCS11_ENCODING_ERROR: A PKCS#11 (Cryptoki) library function was unable to ASN.1 (DER) encode a data structure. See log for more details.
筆者の環境では、リビジョン:9a37b8d6では動作したため、こちらを利用します。前の作業で利用したものを再利用しても良いですが、本手順ではクローンし直して、再ビルドします。
$ git clone https://github.com/MicrochipTech/cryptoauthlib $ cd cryptoauthlib $ git reset --hard 9a37b8d6 $ mkdir build $ cd build $ cmake \ -DATCA_HAL_I2C=ON \ -DATCA_PKCS11=ON \ -DATCA_TNGTLS_SUPPORT=ON \ -DATCA_ATECC608A_SUPPORT=ON \ -DATCA_OPENSSL=ON ../ $ cmake --build . $ sudo make install
以下のコマンドでファイルの生成時刻を確認し、再度インストールされていることを確認します。
$ ls -al /usr/lib/ | grep "libcryptoauth.so"
opensslの設定
MicrochipTech様のPKCS11 Linux Setup|Without using p11-kit-proxyを参考にしました。
OpenSSLがlibp11
とcryptoauthlib
を使用するように手動で設定します。前述したパスをopenssl.cnfに追記します。openssl_init
は先頭に記述する必要があるため注意してください。
デフォルトのコンフィグとのdiffは以下の通りです。
$ diff /usr/lib/ssl/openssl.cnf /usr/lib/ssl/openssl.cnf_bakup 1,14d0 < openssl_conf = openssl_init < [openssl_init] < engines=engine_section < < [engine_section] < pkcs11 = pkcs11_section < < [pkcs11_section] < engine_id = pkcs11 < # Wherever the engine installed by libp11 is. For example it could be: < # dynamic_path = /usr/lib/ssl/engines/libpkcs11.so < dynamic_path = /usr/lib/arm-linux-gnueabihf/engines-1.1/libpkcs11.so < MODULE_PATH = /usr/lib/libcryptoauth.so < init = 0
CSRの作成
CSRを作成します。よくある手順だと、秘密鍵のファイルを指定しますが、ATECC608Aは秘密鍵を読み出せないため、PKCS#11のツールを利用します。
sudo openssl req -engine pkcs11 -key "pkcs11:token=MCHP;object=device;type=private" -keyform engine -new -out new_device.csr -subj "/C=JP/ST=Tokyo/L=Tokyo/O=MyCompany/CN=12345"
以下のコマンドで、CSRの内容を確認できます。
$ openssl req -in new_device.csr -text Certificate Request: Data: Version: 1 (0x0) Subject: C = JP, ST = Tokyo, L = Tokyo, O = MyCompany, CN = 12345 Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: ... ASN1 OID: prime256v1 NIST CURVE: P-256 Attributes: a0:00 Signature Algorithm: ecdsa-with-SHA256 ... -----BEGIN CERTIFICATE REQUEST----- ... -----END CERTIFICATE REQUEST-----
証明書の作成
AWS IoTでは、CSRから証明書を作成する方法があります。今回はそちらを活用します。
マネジメントコンソールで行う場合 CSRによる証明書の作成を参考にしてください。
AWS CLIで行う場合
# 登録したいAWS環境にassume roleする aws iot create-certificate-from-csr \ --certificate-signing-request=file://new_device.csr \ --set-as-active
{ "certificateArn": "arn:aws:iot:ap-northeast-1:111111111111:cert/<certificateId>", "certificateId": "<certificateId>", "certificatePem": "-----BEGIN CERTIFICATE-----***-----END CERTIFICATE-----\n" }
JSONの証明書部分(certificatePem)を、device.crt
として保存しておきます。
# 制御文字を反映させるため-eオプションをつけます。 echo -e "-----BEGIN CERTIFICATE-----***-----END CERTIFICATE-----\n">device.cert
証明書にポリシーをアタッチ
ポリシー定義
echo '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow","Action":["iot:*"],"Resource": ["*"]}]}'|jq>policy.json
ポリシー作成
aws iot create-policy --policy-name reteminal-test \ --policy-document file://policy.json
実行結果
{ "policyName": "reteminal-test", "policyArn": "arn:aws:iot:ap-northeast-1:111111111111:policy/reteminal-test", "policyDocument": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Action\": [\n \"iot:*\"\n ],\n \"Resource\": [\n \"*\"\n ]\n }\n ]\n}\n", "policyVersionId": "1" }
証明書にポリシーをアタッチ。(何も出力されなければ成功です)
aws iot attach-policy \ --policy-name reteminal-test \ --target arn:aws:iot:ap-northeast-1:111111111111:cert/<certificateId>
最終的に以下のようになっていることを確認します。
MQTT通信確認
MQTT通信するために必要なsdkのインストール、通信時に利用するルートCAを取得します。
# awsiotsdkのインストール pip3 install awsiotsdk # ルートCAのダウンロード wget https://www.amazontrust.com/repository/AmazonRootCA1.pem
以下のような配置にします
. ├── AmazonRootCA1.pem # wgetで取得 ├── pkcs.py # 後述するpythonスクリプト └── device.crt # 証明書作成の項目で作成
ソースコードは以下となります。ポイントは、通常秘密鍵を平文で設定していたところを、本コードでは設定していないことです。前述しましたがATECC608AのSLOT0番は、公開鍵は取得出来ても、秘密鍵の書き込み、読み出しは不可能です。故に安全に秘密鍵を管理することが可能です。
from awsiot import mqtt_connection_builder from awscrt import io, http, auth, mqtt import time import json def on_connection_interrupted(connection, error, **kwargs): print("Connection interrupted. error: {}".format(error)) def on_connection_resumed(connection, return_code, session_present, **kwargs): print("Connection resumed. return_code: {} session_present: {}".format(return_code, session_present)) if __name__ == '__main__': pkcs11_lib_path = "/usr/lib/libcryptoauth.so" print(f"Loading PKCS#11 library '{pkcs11_lib_path}' ...") pkcs11_lib = io.Pkcs11Lib( file=pkcs11_lib_path, behavior=io.Pkcs11Lib.InitializeFinalizeBehavior.STRICT) print("Loaded!") mqtt_connection = mqtt_connection_builder.mtls_with_pkcs11( pkcs11_lib=pkcs11_lib, # cryptoauthlibのコンパイル済みshared objectファイルを指定 user_pin="1234", # 適当 token_label="MCHP", # /var/lib/cryptoauthlib/0.confのlabel値を指定 private_key_label="device", # /var/lib/cryptoauthlib/0.confから指定 cert_filepath="./device.crt", # 項「証明書の作成」で作成した証明書 endpoint="***-ats.iot.ap-northeast-1.amazonaws.com", # AWS IoT Coreエンドポイント port=8883, ca_filepath="./AmazonRootCA1.pem", # wgetで取得 on_connection_interrupted=on_connection_interrupted, # 接続時コールバック on_connection_resumed=on_connection_resumed, # 切断時コールバック client_id="myid", # モノに一意は値を指定 clean_session=False, keep_alive_secs=30 ) connect_future = mqtt_connection.connect() connect_future.result() print("Connected!") while True: PUBLISH_TOPIC_NAME = "sample/test" mqtt_connection.publish( topic=PUBLISH_TOPIC_NAME, payload=json.dumps({"hoo": "hoge"}), qos=mqtt.QoS.AT_LEAST_ONCE, ) time.sleep(5)
$ python3 pkcs.py Loading PKCS#11 library '/usr/lib/libcryptoauth.so' ... Loaded! Connected!
publish出来ていることが確認できたらOKです。
最後に
今回はスロット0で試しましたが、スロット2,3,4では、フリーの秘密鍵を設定可能です。フリーの秘密鍵を利用するパターンでも試してみようと思います。恐らく/var/lib/cryptoauthlib/0.conf
の設定を変更すればいけると思います。。